﻿#region Header

/*
Remote views based on linq
By

Pascal Hauser 
Dipl. Ing. in Informatik, Hochschule für Technik Rapperswil, 2006
Master Thesis, Hochschule für Technik Rapperswil, 2008-2010

and

Raphael Gfeller
Dipl. Ing. in Informatik, Hochschule für Technik Rapperswil, 2006
Master Thesis, Hochschule für Technik Rapperswil, 2008-2010

*/

#endregion

#region Usings

using System;
using System.Collections;
using System.Collections.Generic;
using Sebarf.Services.Interfaces;
using Sebastof.Services.Interfaces;

#endregion

namespace Sebastof.Services.Impl {
	/// <summary>
	/// This is a simple cache implementation. The cache works by using a lookup table to work objects
	/// based on their IDs. The reference to an object is stored by using a WeakReference. This, so
	/// that the GC can collect no longer used objects. The cache is self maintaining, which means
	/// that it drops out references to no longer existings objects automatically.
	/// </summary>
	public class SimpleCache : Service, ICachedDataRegistrationService, ICachedDataLocatorService {
		#region Public Properties

		/// <summary>
		/// The property to the lookup table. Based on the ID of an object we hold a WeakReference
		/// </summary>
		public Dictionary<string, WeakReference> InternalValues {
			get { return mm_values; }
		}

		#endregion

		#region Private Fields

		/// <summary>
		/// Auto-Clean-Up interval.
		/// </summary>
		private const int CLEAN_CACHE_ALL_N_ACCESSES = 100;

		/// <summary>
		/// Objects can be marked as invalid. Invalid objects are no longer returnd as they need to be reloaded.
		/// </summary>
		private readonly List<string> m_InvalidatedObjectIds = new List<string>();

		/// <summary>
		/// The lookup table. Based on the ID of an object we hold a WeakReference
		/// </summary>
		private readonly Dictionary<string, WeakReference> mm_values = new Dictionary<string, WeakReference>();

		/// <summary>
		/// A counter for the auto-clean-up.
		/// </summary>
		private int m_cacheCleanupAccessCounter;

		#endregion

		#region ICachedDataLocatorService Members

		/// <summary>
		/// Refreshs the object corresponding to the given key with the new data given.
		/// </summary>
		/// <param name="key"></param>
		/// <param name="freshData"></param>
		public void RefreshObject(string key, Dictionary<string, object> freshData, object freshObject) {
			if (m_InvalidatedObjectIds.Contains(key)) {
				object existingObj = mm_values[key].Target;
				var existingObjIL = existingObj as SebastofApplicationObject;
				if (existingObjIL != null) {
					SebastofApplicationObject.EObserverNotifyState backupObserverNotifyState = existingObjIL.ObserverNotifyState;
					existingObjIL.ObserverNotifyState = SebastofApplicationObject.EObserverNotifyState.DoNotNotifyServers;
					try {
						foreach (var value in freshData) {
							if (!(value.Value is IList)) {
								existingObjIL.SetValue(value.Key, value.Value);
							}
							else {
								//TODO: check collections
							}
						}
						existingObjIL.SetVersion((freshObject as IVersionedObject).GetVersion());
					}
					finally {
						existingObjIL.ObserverNotifyState = backupObserverNotifyState;
					}
				}
				else {
					throw new NotImplementedException();
				}
				m_InvalidatedObjectIds.Remove(key);
			}
		}

		/// <summary>
		/// Returns the object of the key given.
		/// </summary>
		/// <param name="key"></param>
		/// <returns></returns>
		public object GetCachedObject(string key) {
			if (mm_values.ContainsKey(key)) {
				WeakReference e = mm_values[key];
				if (e.IsAlive) {
					return e.Target;
				}
			}
			return null;
		}

		/// <summary>
		/// Returns the typed object of the key given
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <param name="key"></param>
		/// <returns></returns>
		public T GetCachedData<T>(string key) {
			CleanUpCache();
			if (!mm_values.ContainsKey(key)) {
				return default(T);
			}
			WeakReference entry = mm_values[key];
			if (!entry.IsAlive) {
				return default(T);
			}
			return (T)entry.Target;
		}

		/// <summary>
		/// Returns the object of the key given, but returning the given default object (after adding it) if
		/// it not yet exist in the cache.
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <param name="key"></param>
		/// <param name="defaultData"></param>
		/// <returns></returns>
		public T GetCachedData<T>(string key, T defaultData) {
			CleanUpCache();
			if (!mm_values.ContainsKey(key)) {
				AddOrUpdateCachedValue(key, defaultData);
			}
			if (mm_values.ContainsKey(key) && !mm_values[key].IsAlive) {
				AddOrUpdateCachedValue(key, defaultData);
			}
			return GetCachedData<T>(key);
		}

		/// <summary>
		/// return true if the cache contains an object for the given id.
		/// </summary>
		/// <param name="key"></param>
		/// <returns></returns>
		public bool ContainsKey(string key) {
			return mm_values.ContainsKey(key) && mm_values[key].IsAlive;
		}

		#endregion

		#region ICachedDataRegistrationService Members

		/// <summary>
		/// This method adds an object to the cache, if it is not yet existing.
		/// Else the object currently in the cache is replaced with the object given.
		/// </summary>
		/// <param name="key"></param>
		/// <param name="data"></param>
		public void AddOrUpdateCachedValue(string key, object data) {
			lock (this) {
				CleanUpCache();
				if (mm_values.ContainsKey(key)) {
					mm_values[key] = new WeakReference(data);
				}
				else {
					mm_values.Add(key, new WeakReference(data));
				}
			}
		}

		/// <summary>
		/// Invalidates the object with the given key.
		/// </summary>
		/// <param name="key"></param>
		public void InvalidateObject(string key) {
			m_InvalidatedObjectIds.Add(key);
		}

		/// <summary>
		/// removes an entry from the cache
		/// </summary>
		/// <param name="key"></param>
		public void RemoveCachedValue(string key) {
			lock (this) {
				mm_values.Remove(key);
			}
		}

		#endregion

		/// <summary>
		/// Returns the statistics available for the cache.
		/// </summary>
		/// <param name="statisticData"></param>
		/// <returns></returns>
		protected override ServiceStatistics OnCollectStatisticData(ServiceStatistics statisticData) {
			statisticData.AddEntry("AccessCount", (double)m_cacheCleanupAccessCounter);
			statisticData.AddEntry("CachedEntriesCount", (double)mm_values.Count);
			statisticData.AddEntry("CleanCacheAfter % accesses", (double)CLEAN_CACHE_ALL_N_ACCESSES);
			return base.OnCollectStatisticData(statisticData);
		}

		/// <summary>
		/// Checks if the cache needs to be cleaned up and does the clean up if needed.
		/// </summary>
		private void DoCleanUpIfNeeded() {
			m_cacheCleanupAccessCounter++;
			if (m_cacheCleanupAccessCounter % CLEAN_CACHE_ALL_N_ACCESSES == 0) {
				CleanUpCache();
			}
		}

		/// <summary>
		/// Cleans up the cache by removing entries which point to objects no longer alive.
		/// </summary>
		private void CleanUpCache() {
			var toRemove = new List<string>();
			foreach (var entry in mm_values) {
				if (!entry.Value.IsAlive) {
					toRemove.Add(entry.Key);
				}
			}
			foreach (string entry in toRemove) {
				mm_values.Remove(entry);
			}
		}
	}
}